A few months ago when I had first started learning about GraphQL, I had written a previous tutorial for using it with Couchbase and Node.js. The tutorial focused on the basics which included creating GraphQL objects and querying those objects from the NoSQL database, Couchbase. Fast forward a bit and I wrote a tutorial that offered an alternative way to use GraphQL with Node.js, even though the database layer wasn’t the emphasis.
When using or creating an API, there are often scenarios where you don’t want all data to be accessible by everyone. In these scenarios, you’d want some kind of regulation through authorization and API tokens. Like with a RESTful API, this can easily be accomplished through JSON web tokens (JWT).
We’re going to see how to use JWT in a GraphQL application to protect certain pieces of data rather than all or none.
Going forward, it is probably a good idea to have at least a little bit of an understanding of how JSON web tokens (JWT) work. If you’d like to check out a quick getting started guide, check out my tutorial titled, JWT Authentication in a Node.js Powered API. The focus of that tutorial is not GraphQL, but it still works as a good starting point.
The goal for this tutorial is to create an API that uses GraphQL. We’ll be able to create accounts, obtain tokens, and use those tokens to access protected parts of our API. Instructions for installing and configuring Couchbase will not be a part of this tutorial, but nothing special needs to be done for compatibility.
Creating a New Node.js Application with the Project Dependencies
Before we jump into the heavier and more complex parts of our project, let’s create a fresh project with all the dependencies and some boilerplate code. Assuming that you’ve already got Node.js installed and configured, execute the following from your CLI:
1 2 3 4 5 6 7 8 9 |
npm init -y npm install express --save npm install express-graphql --save npm install graphql --save npm install jsonwebtoken --save npm install uuid --save npm install couchbase --save npm install body-parser --save npm install bcryptjs --save |
The above commands will create a new package.json file and install each of our necessary packages. These packages include a GraphQL library as well as a GraphQL extension for Express Framework. We’re also including a way to hash our password data, create JWT, and store everything in Couchbase. Could I have done this with a single line? Yes, but I thought it would be easier to read if I broke it up.
With the project created, go ahead and create an app.js file that includes the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
const Express = require("express"); const Couchbase = require("couchbase"); const BodyParser = require("body-parser"); const JsonWebToken = require("jsonwebtoken"); const Bcrypt = require("bcryptjs"); const ExpressGraphQL = require("express-graphql"); const GraphQLObjectType = require("graphql").GraphQLObjectType; const GraphQLID = require("graphql").GraphQLID; const GraphQLString = require("graphql").GraphQLString; const GraphQLSchema = require("graphql").GraphQLSchema; const GraphQLList = require("graphql").GraphQLList; const UUID = require("uuid"); var cluster = new Couchbase.Cluster("couchbase://localhost"); cluster.authenticate("example", "123456"); var bucket = cluster.openBucket("example"); var app = Express(); app.use(BodyParser.json()); app.set("jwt-secret", "polyglotdeveloper"); app.use("/graphql", ExpressGraphQL({ graphiql: true })); app.post("/register", (request, response) => { }); app.post("/login", (request, response) => { }); app.listen(3000, () => { console.log("Listening at :3000"); }); |
We’re going to call the above code our boilerplate code. Essentially we’re just doing some setup and nothing truly relevant to our end goal. We’re importing our downloaded packages, establishing a connection to Couchbase, configuring Express Framework, defining three endpoints, and serving on port 3000.
Make sure to use your own Couchbase information rather than the information I used. Also, for better security, change the jwt-secret
so your tokens are hashed in a less predictable fashion than mine.
Designing Simple Account API endpoints for Login and Registration
You’ll notice from our boilerplate code that we have an endpoint for both registration and sign-in. There are many ways to accomplish what we’re trying to do, probably some better than my solution. However, I found what we’re about to see logical and easy to implement without getting too far away from our goals.
Starting with the /register
endpoint, we have the following:
1 2 3 4 5 6 7 8 9 10 11 |
app.post("/register", (request, response) => { var id = UUID.v4(); request.body.type = "account"; request.body.password = Bcrypt.hashSync(request.body.password, 10); bucket.insert(id, request.body, (error, result) => { if(error) { return response.status(500).send({ code: error.code, message: error.message }); } response.send(request.body); }); }); |
When a user sends a POST request with username
and password
in the body, we are first creating a new UUID to be used as our Couchbase document key, then we are hashing the password using Bcrypt, like previously demonstrated in a tutorial I wrote.
Once a hash is created, we store the profile data in Couchbase and return it. The whole goal behind the /register
endpoint is to give us some data to authenticate with. To authenticate, we use the /login
endpoint as seen below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
app.post("/login", (request, response) => { var statement = "SELECT META(account).id, account.username, account.`password` FROM `" + bucket._name + "` AS account WHERE account.type = 'account' AND account.username = $username"; var query = Couchbase.N1qlQuery.fromString(statement); bucket.query(query, { "username": request.body.username }, (error, account) => { if(error) { return response.status(500).send({ code: error.code, message: error.message }); } Bcrypt.compare(request.body.password, account[0].password, function(error, result) { if(error || !result) { return response.status(401).send({ "success": false, "message": "Invalid username and password" }); } var token = JsonWebToken.sign(account[0].id, app.get("jwt-secret"), {}); response.send({"token": token}); }); }); }); |
In the above code, we are creating a N1QL query to find a document that contains a username that matches the username that was passed with the request. We’re using a parameterized query to prevent any kind of SQL injection attack.
If a document was found, we then compare the stored hashed password with the password that was passed in with the request. If it matches, we can sign the user id using our secret, giving us a JWT to be used in future requests.
A few things to note with our two endpoints:
- We’re not doing any data validation. This is an example and we’re trying to keep things simple.
- We’re not setting an expiration on our JWT. Typically you’d want it to expire within an hour, but this is just a simple example.
As of now, we’ve done nothing with GraphQL. We’ve just laid down the foundation for creating users and getting JSON web tokens. Even though we’re creating JWT, we are not validating that they are accurate as of now.
Validating JSON Web Tokens (JWT) with an Express Framework Function
When it comes to working with protected data, having a JWT is not enough. We want to make sure the token is in fact valid by checking the signature. We also need a solution for passing JWT around.
Express Framework does have middleware for working with JSON web tokens, but I found the documentation to be lacking. Instead, it seemed easier to just create my own method for validation. Take the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
app.use((request, response, next) => { var authHeader = request.headers["authorization"]; if(authHeader) { var bearerToken = authHeader.split(" "); if(bearerToken.length == 2 && bearerToken[0].toLowerCase() == "bearer") { JsonWebToken.verify(bearerToken[1], app.get("jwt-secret"), function(error, decodedToken) { if(error) { return response.status(401).send("Invalid authorization token"); } request.decodedToken = decodedToken; next(); }); } else { next(); } } else { next(); } }); |
So what are we doing in the above code?
Since we’re using app.use
, on every request, this function will be called. When this function is called, we look at the current request headers and look for an authorization header. If an authorization header exists, we make sure it is a bearer token and use the actual token value in our verification. The actual token is our JWT. If the token is valid, we can add the decoded value to the request which then becomes accessible in the next stage of our request. If no token is present, no worries because nothing will happen. Our logic only checks our tokens if they exist because not all data points will be protected.
Are there better ways to obtain our JWT token in a request and validate it? Probably, but the above code worked when I tested it out and it wasn’t too complicated.
Developing a partially protected API with GraphQL and JavaScript
Now that we have the JWT logic in place, we can focus on the development of our API. Technically the /login
and /register
endpoints are part of our API, but the focus is GraphQL.
Before we worry about the queries, let’s focus on our GraphQL objects:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const AccountType = new GraphQLObjectType({ name: "Account", fields: { id: { type: GraphQLID }, username: { type: GraphQLString }, password: { type: GraphQLString } } }); const CourseType = new GraphQLObjectType({ name: "Course", fields: { id: { type: GraphQLID }, title: { type: GraphQLString }, length: { type: GraphQLString }, author: { type: AccountType } } }); |
Remember, if you saw my previous Couchbase with GraphQL tutorial, the objects above probably look a little different. This is because I modeled them after my alternative GraphQL tutorial. Essentially, we have two objects where one is for account data and the other is for course data. Maybe not the best example, but we’ll make it work.
The course data will reference an account for the course author. We’ll be creating a resolver method for author information, but not yet. Let’s first create a query for account data separately:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { account: { type: AccountType, resolve: (root, args, context, info) => { return new Promise((resolve, reject) => { if(!context.decodedToken) { return reject("A valid authorization token is required"); } var statement = "SELECT META(account).id, account.username, account.`password` FROM `" + bucket._name + "` AS account WHERE account.type = 'account' AND META(account).id = $id"; var query = Couchbase.N1qlQuery.fromString(statement); bucket.query(query, { "id": context.decodedToken }, (error, result) => { if(error) { return reject(error.message); } resolve(result[0]); }); }); } } } }) }); |
It only makes sense that when we have a query like account
, we want to get data for a particular account and that should probably be our own account. If we’re querying our own account, we should probably have a valid JWT.
Notice how we’re using context.decodeToken
in the above code. The context
allows us to get data from the request and in our case, the decoded token data might exist in the request. If it doesn’t exist, we should probably throw an error because a valid token is a requirement for this query.
If we have a valid token, we can create a N1QL query and use the user id to query for our account and return it.
Not bad right?
Let’s expand on our queries. Within the fields
object, we’re going to add another query, but this time we’ll be querying for course data and course data won’t necessarily be protected.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
courses: { type: GraphQLList(CourseType), resolve: (root, args, context, info) => { return new Promise((resolve, reject) => { var statement = "SELECT META(course).id, course.title, course.length, course.author FROM `" + bucket._name + "` AS course WHERE course.type = 'course'"; var query = Couchbase.N1qlQuery.fromString(statement); bucket.query(query, (error, result) => { if(error) { return reject(error.message); } resolve(result); }); }); } } |
In the above code, we are doing a simple N1QL query for all courses stored in the database. The catch here is that for author data, we’re only obtaining the key, not the fully loaded account data which is protected.
We’re actually going to do some data manipulations in the GraphQL object for CourseType
instead:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
const CourseType = new GraphQLObjectType({ name: "Course", fields: { id: { type: GraphQLID }, title: { type: GraphQLString }, length: { type: GraphQLString }, author: { type: AccountType, resolve: (root, args, context, info) => { return new Promise((resolve, reject) => { if(!context.decodedToken || context.decodedToken != root.author) { return reject("A valid authorization token is required"); } var statement = "SELECT META(account).id, account.username, account.`password` FROM `" + bucket._name + "` AS account WHERE account.type = 'account' AND META(account).id = $id"; var query = Couchbase.N1qlQuery.fromString(statement); bucket.query(query, { "id": root.author }, (error, result) => { if(error) { return reject({ code: error.code, message: error.message }); } resolve(result[0]); }); }); } } } }); |
Notice that we now have a resolve
function on the author
property. In this function, we check for a valid token and if we don’t find one, we return an error. This check and error only happens if the author
data is requested. If the query does not request author
data, we can get the other information without a token since it is not protected.
There is another catch here. Not only are we checking to make sure a valid JWT is present, but we also want the JWT to match the data returned in the N1QL query. This means if we have several authors, only the authors we have permission for will succeed. Again, not the best example, but it proves our point.
To clean up any loose ends, we need to update our /graphql
endpoint:
1 2 3 4 |
app.use("/graphql", ExpressGraphQL({ schema: schema, graphiql: true })); |
All we’ve done is add our schema
which includes our two possible queries.
Conclusion
While our example was simple, it allowed us to demonstrated protected queries and pieces of data with GraphQL, Couchbase, and JSON web tokens (JWT). Some core things to remember:
- Request data can be accessed in the GraphQL
context
variable. - JSON web tokens and accounts should probably be created through separate endpoints rather than with GraphQL mutations.
- You can restrict queries as well as data properties with JWT. It is not an all or nothing series of events.
If you want to learn more about GraphQL, I suggest you check out my previous tutorial on the subject. To learn more about Couchbase with Node.js, check out the Couchbase Developer Portal.